Skip to main content

Performance tips

Performance Tips

You can explicitly choose what will be interpreted on the frontend or backend. There are a few possibilities for our function inside the Line expression.

Full Load on the Kernel

For this, one needs to modify the code to:

EventHandler[InputRange[0,4,0.1], Function[data, 
lines = With[{y = data},
Table[{Cos[x], Sin[y x]}, {x,0,2Pi, 0.01}]
]
]];
% // EventFire (* Just to initialize *)

The last line manually fires an event to initialize the symbol lines. Then, for the output, we can write:

Graphics[{Cyan, Line[lines // Offload]}]

This binding can be illustrated as shown in the image below:

Using the Frontend

One can move the entire Table computation to the browser's side. Let's discard our previous changes:

EventHandler[InputRange[0,4,0.1], Function[data, 
v = data
]];
% // EventFire
Naive Approach 1

A straightforward solution for output could be:

Graphics[{Cyan, Line[
Table[{Cos[x], Sin[Offload[v] x]}, {x,0,2Pi, 0.1}]
]}]

This would be a terrible solution 👎🏼

Each time the Table iterator x goes through the range of values, it creates a sublist of Sin and Cos functions that contain the dynamic variable v. This results in multiple instances of v.

danger
Line[Table[Expression[Offload[symbol]], {i, 10}]]

Creates 10 instances of symbol. The Line function will be called 10 times on each update of symbol!

danger

Avoid placing dynamic symbols inside large Table expressions. Minimize the number of copies created.

Naive Approach 2

Let's try to improve it a bit:

Graphics[{Cyan, Line[
Table[{Cos[x], Sin[v x]}, {x,0,2Pi, 0.1}] // Offload
]}]

This is also inefficient 👎🏼 The Table function still runs on the browser's side.

Optimized Version

One can reduce the number of instances to just one using With, as shown in the example above:

Graphics[{Cyan, Line[
With[{y = v},
Table[{Cos[x], Sin[y x]}, {x,0,2Pi, 0.01}]
] // Offload
]
}]

This saves a lot of resources 👍🏼

tip
Line[With[{y = symbol}, Table[AnyExpression[y], {i, 10}]]]

Creates only 1 instance of symbol. The Line function will be called once per update of symbol.

tip
Line[symbol//Offload], ... Line[symbol//Offload]

This is acceptable since each Line is bound to its own symbol instance. Therefore, on an update of symbol, each Line expression will be reevaluated once.

If Duplicating Is Unavoidable

If you need to update two properties of a dynamic expression, such as GraphicsComplex (which has VertexColors and a list of vertices), it is unavoidable to use two Offload calls:

GraphicsComplex[vertices // Offload, {Polygon[triangles]}, "VertexColors"->Offload[colors]]

If later in the code:

vertices = ...;
colors = ...;

Both vertices and colors will cause the reevaluation of GraphicsComplex twice for the same data set. However, reevaluation can be reduced using options of Offload:

GraphicsComplex[vertices // Offload, {Polygon[triangles]}, "VertexColors"->Offload[colors, "Static"->True]]

Here, colors will not be bound to GraphicsComplex. This results in only a single reevaluation per update of colors and vertices. However, new values will still be read once vertices has been updated.

A Possible Pitfall with With

There might be a temptation to wrap the Line expression inside With, like this:

Graphics[{Cyan, With[{y = v}, 
Line[
Table[{Cos[x], Sin[y x]}, {x,0,2Pi, 0.01}]
]
] // Offload}]

This will not work at all 👎🏼 because the binding will occur between Graphics and v objects.

Think of an onion from the Shrek movie!

Numeric Arrays

info

It is usually done automatically by Wolfram Language if the data has been generated using pure functions.

When transferring points as nested lists, it is better to wrap them in NumericArray. This informs the WLJS Interpreter in the browser that only numbers or lists of numbers are expected, reducing the load during parsing.

For example, using dynamic symbols:

(* Every update *)
symbol = someFunctionThatReturnsList

20 FPS

By using NumericArray:

(* Every update *)
symbol = NumericArray[someFunctionThatReturnsList]

~40 FPS